package com.tianji.learning.service.impl;

import cn.hutool.core.util.ObjectUtil;
import com.tianji.api.client.course.CourseClient;
import com.tianji.api.dto.course.CourseFullInfoDTO;
import com.tianji.api.dto.leanring.LearningLessonDTO;
import com.tianji.api.dto.leanring.LearningRecordDTO;
import com.tianji.common.exceptions.DbException;
import com.tianji.common.utils.AssertUtils;
import com.tianji.common.utils.BeanUtils;
import com.tianji.common.utils.ObjectUtils;
import com.tianji.common.utils.UserContext;
import com.tianji.learning.domain.dto.LearningRecordFormDTO;
import com.tianji.learning.domain.enums.LessonStatus;
import com.tianji.learning.domain.enums.SectionType;
import com.tianji.learning.domain.po.LearningLesson;
import com.tianji.learning.domain.po.LearningRecord;
import com.tianji.learning.mapper.LearningRecordMapper;
import com.tianji.learning.service.ILearningLessonService;
import com.tianji.learning.service.ILearningRecordService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.learning.utils.LearningRecordDelayTaskCheckHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/**
 * <p>
 * 学习记录表 服务实现类
 * </p>
 *
 * @author xie
 * @since 2024-11-10
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class LearningRecordServiceImpl extends ServiceImpl<LearningRecordMapper, LearningRecord> implements ILearningRecordService {


    private final ILearningLessonService learningLessonService;
    private final CourseClient courseClient;

    private final LearningRecordDelayTaskCheckHandler taskHandler;


    @Override
    public LearningLessonDTO queryLearningRecordByCourse(Long courseId) {
        log.info("查询用户指定课程：{}的学习进度", courseId);

        Long user = UserContext.getUser();

        //根据课程id和用户id查询课表信息（需要两个字段：课表id和最后观看小节id）
        LearningLesson lesson = learningLessonService.lambdaQuery()
                .eq(LearningLesson::getUserId, user)
                .eq(LearningLesson::getCourseId, courseId)
                .one();

        if (ObjectUtil.isEmpty(lesson)) {
            return null;
        }

        //根据课表信息查询课程记录信息
        List<LearningRecord> records = this.lambdaQuery()
                .eq(LearningRecord::getLessonId, lesson.getId()).list();

        //组装课表信息和课程记录信息返回
        List<LearningRecordDTO> learningRecordDTOS = BeanUtils.copyList(records, LearningRecordDTO.class);

        return LearningLessonDTO.of(lesson.getId(), lesson.getLatestSectionId(), learningRecordDTOS);

    }


    /**
     * 提交学习记录
     *
     * @param courseId 课程id
     * @return
     */
    @Override
    @Transactional
    public void submitLearningRecord(LearningRecordFormDTO formDTO) {

        //formDTO进来会有六个参数：课表id，小节id，小节类型，提交时间，视频时长，播放进度
        //根据视频或者考试进入不同分支
        boolean firstFinished = formDTO.getSectionType() == SectionType.VIDEO ? handlerVideoRecord(formDTO) : handlerExamRecord(formDTO);
        if(!firstFinished && formDTO.getDuration() == -1){
            return;
        }

        //firstFinished是标志位：true说明该小节学完，false，则说明没学完
        handlerLearningLesson(firstFinished, formDTO);

    }

    /**
     * 修改课表数据
     *
     * @param firstFinished
     * @param formDTO
     */
    private void handlerLearningLesson(boolean firstFinished, LearningRecordFormDTO formDTO) {

        //利用fromDTO的课表id查询课表的数据
        LearningLesson lesson = learningLessonService.getById(formDTO.getLessonId());
        AssertUtils.isNotNull(lesson, "课表数据不存在，无法更新数据");

        //等于true则说明课程已经全部学完，false则没学完，这只是一个全部学完的依据，其实什么都没做，真正的更新在第三步
        boolean allLearned = false;

        //此处的firstFinished就是是否已完成这个小节学习的标志位，比如考试，一定是完成，返回的这个标志为就一定是true，所以会进入判断
        if (firstFinished) {

            //利用课表的课程id，到课程远程调用中获取该课程的信息（这一步主要是想获取课程的总课程数）
            CourseFullInfoDTO courseInfoById = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
            AssertUtils.isNotNull(courseInfoById, "课程数据不存在");

            //2.由于进入这个判断就标志着已经学完了一个小节，所以在课表的已学完字段+1之后（表示学完+1），再判断是否等于总课程数，
            // 如果等于总课程数，说明这个小节学完后，就已经学完了整个课程，返回true
            allLearned = lesson.getLearnedSections() + 1 >= courseInfoById.getSectionNum();
        }


        //3.更新课表
        boolean update = this.learningLessonService.lambdaUpdate()
                .set(LearningLesson::getLatestSectionId, formDTO.getSectionId()) //最近学习小节id，这个是小节学没学完都要更新的
                .set(LearningLesson::getLatestLearnTime, formDTO.getCommitTime()) //最近学习时间，这个是小节学没学完都要更新的
                .setSql(firstFinished, "learning_sections = learned_sections + 1")//这个是只有传进来的firstFinished（完成一个小节的学习的标志）为true，说明已学完，则直接在课表的学习总记录数+1
                .set(lesson.getStatus() == LessonStatus.NOT_BEGIN, LearningLesson::getStatus, LessonStatus.LEARNING)//如果学习状态没开始，则将未学习的状态改为学习中
                .set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED)//判断如果所有的小节数都学完的标志为true，则将这个课表数据改为已学完
                .eq(LearningLesson::getId, lesson.getId())//课表id
                .update();

        if (!update) {
            throw new DbException("修改课表数据失败！");
        }

    }

    /**
     * 处理考试学习记录数据（表）
     *
     * @param formDTO
     * @return
     */
    private boolean handlerExamRecord(LearningRecordFormDTO formDTO) {

        //1.由于前端只传了5个参数，要将记录保存到数据库中，需要转换一下类型
        LearningRecord learningRecord = BeanUtils.copyBean(formDTO, LearningRecord.class);

        //2.转换后的类型少了一些填充缺失的数据，目的是微课添加到记录表中
        //2.1填充用户id
        learningRecord.setUserId(UserContext.getUser());
        //2.2填充是否完成
        learningRecord.setFinished(true);
        //2.3填充提交完成时间
        learningRecord.setFinishTime(formDTO.getCommitTime());

        //3.将记录保存到数据库
        boolean save = save(learningRecord);

        if (!save) {
            throw new DbException("新增考试学习记录失败");
        }
        //返回标志位为：true，表示这个小节已学完
        return true;
    }


    /**
     * 处理视频学习记录
     *
     * @param formDTO
     * @return
     */
    private boolean handlerVideoRecord(LearningRecordFormDTO formDTO) {

        LearningRecord oldRecord = queryOldRecord(formDTO.getLessonId(), formDTO.getSectionId());


        //2.判断是否有旧的学习记录存在，如果不存在
        if (ObjectUtil.isEmpty(oldRecord)) {
            //如果不存在记录则说明这个小节从来没有看过，添加一条新记录到课程记录表中

            //2.1将DTO转为Po
            LearningRecord learningRecord1 = BeanUtils.copyBean(formDTO, LearningRecord.class);
            //2.2补齐字段
            learningRecord1.setUserId(UserContext.getUser());
            //2.3添加到数据库
            boolean save = save(learningRecord1);
            if (!save) {
                throw new DbException("新增学习记录失败！");
            }
            //第一次添加记录，这个小节肯定没学完，所以标志位返回false
            return false;
        }


        //如果存在，更新学习记录
        //第一次学完则为true，否则为false
        boolean firstFinished = !oldRecord.getFinished() && oldRecord.getMoment() << 1 > formDTO.getDuration();



        // 4.2、如果是非第一次学完，将学习记录暂存至redis，并且提交一个延时检测任务
        if(!firstFinished){
            LearningRecord learningRecord = new LearningRecord();
            learningRecord.setLessonId(formDTO.getLessonId());
            learningRecord.setSectionId(formDTO.getSectionId());
            learningRecord.setId(oldRecord.getId());
            learningRecord.setMoment(formDTO.getMoment());
            learningRecord.setFinished(oldRecord.getFinished());

            taskHandler.addLearningRecordDelayCheckTask(learningRecord);
            formDTO.setDuration(-1);
            return false;
        }




        //更新学习记录（一定要更新的：moment，第一次学完要更新的字段：finished，finish_time）
        boolean updateResult = this.lambdaUpdate()
                .set(LearningRecord::getMoment, formDTO.getMoment())
                .set(LearningRecord::getFinished, true)//只有firstFinished为true，也就是第一次学完，才会将是否学完改为true
                .set(LearningRecord::getFinishTime, formDTO.getCommitTime())//只有firstFinished为true，也就是第一次学完，学完时间字段才会被修改
                .eq(LearningRecord::getId, oldRecord.getId())//需要修改的记录的id
                .update();

        if (!updateResult) {
            throw new DbException("更新视频学习记录失败！");
        }

        //清理redis缓存中的学习记录（目的：是为了保证Redis和mysql数据库的双写一致性）
        taskHandler.cleanLearningRecordCache(formDTO.getLessonId(),formDTO.getSectionId());
        return true;
    }

    /**
     * 查询旧的学习记录
     * @param lessonId 课表id
     * @param sectionId 小节id
     * @return
     */
    private LearningRecord queryOldRecord(Long lessonId, Long sectionId) {
        // 1、先从缓存中查询学习记录
        LearningRecord oldRecord = taskHandler.readLearningRecordFromCache(lessonId,sectionId);

        // 2、如果存在，直接返回
        if(ObjectUtils.isNotEmpty(oldRecord)){
            return oldRecord;
        }

        // 3、如果不存在，则查询数据库，并将结果存入缓存
        // select * from learning_record where lesson_id = #{} and section_id = #{}
        oldRecord = this.lambdaQuery().eq(LearningRecord::getLessonId, lessonId)
                .eq(LearningRecord::getSectionId, sectionId).one();
        if(ObjectUtils.isEmpty(oldRecord)){
            return null;  // 不能抛异常，要作为调用者后续代码逻辑的判断依据
        }

        // 4、将数据库的查询结果写入缓存中
        taskHandler.writeLearningRecordToCache(oldRecord);

        // 5、返回record
        return oldRecord;
    }


}




















